import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
from pyreadstat import pyreadstat
from tabulate import tabulate
from scipy import stats
from scipy.stats import somersd
from pandas.api.types import CategoricalDtype
from scipy.stats import norm

plt.rcParams["font.sans-serif"] = ["SimHei"]  # 设置字体


def 读取SPSS数据文件(文件位置及名称, 是否保留标签值: bool):
    数据表, metadata = pyreadstat.read_sav(
        文件位置及名称, apply_value_formats=是否保留标签值, formats_as_ordered_category=True)
    return 数据表


def p值判断(p: float, α=0.05):
    """ p值判断 """
    if p <= α:
        return '拒绝虚无假设'
    else:
        return '接受虚无假设'


def 相关系数判断(系数: int):
    """
    判断相关系数的强弱

    """
    if 系数 >= 0.8:
        return '极强相关'
    elif 系数 >= 0.6:
        return '强相关'
    elif 系数 >= 0.4:
        return '中等强度相关'
    elif 系数 >= 0.2:
        return '弱相关'
    else:
        return '极弱相关或无相关'


def 有序变量描述统计函数(表名, 变量名):
    result = 表名[变量名].value_counts(sort=False)
    描述统计表 = pd.DataFrame(result)
    描述统计表['比例'] = 描述统计表['count'] / 描述统计表['count'].sum()
    描述统计表['累计比例'] = 描述统计表['比例'].cumsum()
    return 描述统计表


def 绘制柱状图(表名):
    x = 表名.index
    y = 表名['count'].values
    fig, ax2 = plt.subplots()
    ax2.bar(x, y)
    plt.show()


def 两个无序类别变量的统计分析(数据表, 自变量, 因变量):
    """ 对两个无序类别变量进行描述统计和推论统计，并给出辅助结论 """
    # 计算相关系数
    tau_y = goodmanKruska_tau_y(数据表, 自变量, 因变量)
    # 制作交互分类表
    交互表 = pd.crosstab(数据表[F"{自变量}"], 数据表[F"{因变量}"])
    # 进行卡方检验
    chi2, p, dof, ex = stats.chi2_contingency(交互表)

    print(F"tau_y系数:{tau_y: 0.4f}", 相关系数判断(tau_y))
    print(tabulate(交互表))
    print(F"卡方值：{chi2: .2f}, p值：{p: .4f},自由度:{dof}。")
    print(p值判断(p))


def 两个有序类别变量的统计分析(数据表, 自变量, 因变量):
    """ 对两个有序类别变量进行描述统计和推论统计，并给出辅助结论 """
    x = 数据表[F"{自变量}"].cat.codes
    y = 数据表[F"{因变量}"].cat.codes
    result = somersd(x, y)
    # 制作交互分类表
    交互表 = pd.crosstab(数据表[F"{自变量}"], 数据表[F"{因变量}"])
    d_y = result.statistic
    p = result.pvalue

    print(F"Somers dy系数:{d_y: 0.4f}", 相关系数判断(d_y))
    print(tabulate(交互表))
    print(F"p值：{p: .4f}")
    print(p值判断(p))


def 两个数值变量的统计分析(数据表, 自变量, 因变量):
    """ 对两个数值变量进行统计分析，并给出辅助结论 """

    x = 数据表[自变量]
    y = 数据表[因变量]
    r, p = stats.pearsonr(x, y)

    fig = px.scatter(数据表, x="22、对于新事物，我喜欢去尝试和体验", y="总分", trendline='ols')
    fig.show()

    print(FR"决定系数r平方：{r*r :0.4f}")
    print(决定系数强弱判断(r*r))
    print(F"p值：{p: .4f}")
    print(p值判断(p))


def 类别变量与数值变量统计分析(数据表, 类别变量, 数值变量):
    """ 对数值变量的不同类别进行对比，并给出辅助结论 """
    from statsmodels.formula.api import ols

    fig = px.box(数据表, x=类别变量, y=数值变量)
    fig.show()

    model = ols(F'{数值变量} ~ {类别变量}', 数据表).fit()

    print(F"相关比率：{model.rsquared}")
    # print(model.summary())
    return model.rsquared, model.summary()


def 决定系数强弱判断(决定系数: float):
    """ 
    <0.25       <0.06	    微弱相关或不相关
    0.25≤≤0.5	0.06≤≤0.25	低度相关
    0.5≤≤0.75	0.25≤≤0.56	中度相关
    >0.75	    >0.56	     高度相关 
    """
    if 决定系数 > 0.56:
        return "高度相关"
    elif 决定系数 > 0.25:
        return "中度相关"
    elif 决定系数 > 0.26:
        return "低度相关"
    else:
        return "微弱相关或不相关"


def 相关比率强弱判断(相关比率: float):
    """ 
    小于 0.01	微弱相关或不相关
    [0.01-0.06]	低度相关
    [0.06-0.14]	中度相关
    [0.14-0.99]	高度相关
    1	        完全相关
    """
    if 相关比率 > 0.14:
        return "高度相关"
    elif 相关比率 > 0.06:
        return "中度相关"
    elif 相关比率 > 0.01:
        return "低度相关"
    else:
        return "微弱相关或不相关"


def 一个总体比例的检验(n, n1, pi0):
    """
    函数功能：一个总体比例的检验
    n:   样本规模，如：共抽取了2000人。
    n1:  某个特征的规模，如：抽取的2000人中，有450人收看了某节目。n1=450
    pi0: 需要检验的总体比例。 
    参考文献：贾俊平. (2021). 统计学——Python实现. 高等教育出版社.
    """
    n = n
    p = n1 / n
    pi0 = pi0
    z = (p - pi0) / np.sqrt(pi0 * (1 - pi0) / n)
    p_value = 1 - norm.cdf(z)
    return p, z, p_value


def 检验两个总体中的比例值是否相等(n1, n2, p1, p2):
    """
    检验两个总体中的比例值是否相等
    研究假设：pi1-pi2≠0
    原假设：pi1-pi2=0
    n1:样本1的规模，如男生200人
    n2:样本1的规模，如女生200人
    p1:样本1的比例，如男生200人中，有27%的赞成某一举措。
    p2:样本2的比例，如女生200人中，有35%的赞成某一举措。
    """
    n1 = n1
    n2 = n2
    p1 = p1
    p2 = p2
    p = (p1 * n1 + p2 * n2) / (n1 + n2)
    z = (p1 - p2) / np.sqrt(p * (1 - p) * (1 / n1 + 1 / n2))
    p_value = norm.cdf(z)
    return (z, p_value)


def gen_mcq_df(df, x, pattern='┋'):
    """
    定义一个针对问卷星数据多选题的分析函数
    df: 数据框
    x: 问卷星中定义的多选题
    返回值： 一个包含所有选项及出现次数和所占比例的数据框
    如：
    |选项|次数|比例（%）|
    |--|--|--|
    |单纯根据个人喜爱|20|5.5|
    |广告内容较感兴趣|20|5.5|
    |喜欢的明星代言|20|5.5|
    |视频广告|20|5.5|
    """
    # 按照指定分隔符将多选题字符串转化包含多个选项的列表
    df['temp'] = df[x].str.split(pattern)
    # 初始化列表，用于保存所有多选题选项
    mcq_items = []
    # 循环所有个案，获取所有多选题选项
    for g in df['temp']:
        for label in g:
            # print(label)
            mcq_items.append(label)
    # 将多选题选项去重后转化为列表，方便构造dataframe
    result = list(set(mcq_items))
    # 构造包含选项、次数和比例的空表
    df_mcq_1 = pd.DataFrame(data=np.zeros([len(result), 2]),
                            index=result,
                            columns=['次数', '比例'])
    # 通过循环获取每个选项在多选题中累次出现的次数
    for i in df[x]:
        for label in result:
            if str(i).__contains__(label):
                df_mcq_1.loc[label, '次数'] += 1
    # 生成比例列
    df_mcq_1['比例'] = df_mcq_1['次数'] / df.shape[0] * 100

    return df_mcq_1.astype({'次数': "int"})


def set_ordered_by_meta(metadata, df, col):
    """ 依据SPSS中的值标签及其顺序，将指定变量设定为有序变量 """
    cat_dtype = CategoricalDtype(
        categories=metadata.variable_value_labels[col].values(), ordered=True)
    return df.astype({col: cat_dtype})


def p_result(p: float) -> str:
    """
    根据接收到的p值，给出判断结果字典。

    参数：
    p： 各种检验得到的p值。

    返回值：
    以字典形式保存的判断结果。
    """
    if p >= 0.05:
        return {
            'p': f'p={round(p,3)}>0.05',
            'tex_p': 'p>0.05',
            'conclusion': '接收虚无假设，拒绝研究假设。',
        }
    elif p >= 0.01:
        return {
            'p': f'p={round(p,3)}<0.05',
            'tex_p': 'p<0.05',
            'conclusion': '拒绝虚无假设，接收研究假设。',
        }
    elif p >= 0.001:
        return {
            'p': f'p={round(p,3)}<0.01',
            'tex_p': 'p<0.01',
            'conclusion': '拒绝虚无假设，接收研究假设。',
        }
    else:
        return {
            'p': f'p={round(p,3)}<0.001',
            'tex_p': 'p<0.001',
            'conclusion': '拒绝虚无假设，接收研究假设。',
        }


def to_sav(df, metadata, filename):
    """
    保存SPSS文件

    要以SPSS格式保存时，Dataframe中变量的值需要以编码的数字形式保存。
    """
    pyreadstat.write_sav(df,
                         filename,
                         variable_value_labels=metadata.variable_value_labels,
                         variable_measure=metadata.variable_measure,
                         variable_format=metadata.original_variable_types)


def get_key(dct, value):
    """ 获取字典中指定值对应的第一个键 """
    result = [k for (k, v) in dct.items() if v == value]
    if result:
        return result[0]
    else:
        return None


def label_to_code(x, col, metadata):
    """ 获取指定值对应的编码值 """
    return get_key(metadata.variable_value_labels[col], x)


def set_label_to_code(df, metadata):
    """ 将SPSS文件中的文字标签转化为数值 """
    for col in df.columns[df.dtypes == 'category']:
        df[col] = df[col].apply(label_to_code, col=col, metadata=metadata)
    return df


def draw_on_eta2(coefficient: int) -> str:
    """
    根据coefficient的值给出建议内容
    按照J.Cohen 提出的标准，0.01时为小效应，0.06时为中等效应，而0.14为大效应。
    Ferguson( 2009) 总结的社会科学领域小0.04、中0.25、大0.64三种参数水平所对应的各类效应量
    指标临界参考值比 Cohen( 1992) 提出的标准更为严格。
    """
    result = ''
    if coefficient >= 0.14:
        result = '高度相关'
    elif coefficient >= 0.06:
        result = '中度相关'
    elif coefficient >= 0.01:
        result = '低度相关'
    else:
        result = '极弱相关或不相关'
    return result


def draw_on_r(coefficient: int) -> str:
    """根据coefficient的值给出建议内容"""
    result = ''
    if coefficient >= 0.56:
        result = '极强相关'
    elif coefficient >= 0.25:
        result = '中等'
    elif coefficient >= 0.06:
        result = '低度相关'
    else:
        result = '极弱相关或不相关'
    return result


def gen_scatter(df, x, y) -> str:
    """
    功能：

    生成散点图

    参数：

    df： DataFrame
    col: DataFrame中的变量，应该为类别变量
    """

    set_plt()

    # 准备数据
    # print(df[x].dtype)
    if df[x].dtype == 'category':
        x1 = df[x].cat.codes
    else:
        x1 = df[x]
    if df[y].dtype == 'category':
        y1 = df[y].cat.codes
    else:
        y1 = df[y]

    # 绘图
    # 创建图
    fig, ax = plt.subplots()
    # 绘制散点图
    # print(x, y)
    art1 = ax.scatter(x1, y1)
    # 设定细节内容
    # 设置x轴变量名称
    ax.set_xlabel(f'{x}')
    # 设置y轴最大值
    ax.set_ylabel(f'{y}')
    plt.show()


def draw_on_corr(coefficient: int) -> str:
    """根据coefficient的值给出建议内容"""
    result = ''
    if coefficient >= 0.8:
        result = '极强相关'
    elif coefficient >= 0.6:
        result = '强相关'
    elif coefficient >= 0.4:
        result = '中等相关'
    elif coefficient >= 0.2:
        result = '弱相关'
    else:
        result = '极弱相关或不相关'
    return result


def goodmanKruska_tau_y(df, x: str, y: str) -> float:
    '''
    古德曼和克鲁斯卡尔 tau(Goodman and Kruskal's tau measure)
    是一种计算两个定类变量相关程度的系数，值介于0-1之间，具有消除误差比例的意义。
    该方法的特点是在计算系数值时，会包含所有的边缘次数和条件次数，故其敏感性高于Lambda系数。
    如果两个变量是不对称关系，最好选用tau-y来简化两个变量的相关情况。

    参考文献：
    李沛良. (2001). 社会研究的统计应用. 社会科学文献出版社

    参数：

    df：DataFrame
    x: 作为自变量的定类变量
    y：作为因变量的定类变量

    返回值：
    tau-y系数，介于0-1之间
    '''
    # 取得条件次数表
    cft = pd.crosstab(df[y], df[x], margins=True)
    # 取得全部个案数目
    n = cft.at['All', 'All']
    # 初始化变量
    E_1 = E_2 = tau_y = 0

    # 计算E_1
    for i in range(cft.shape[0] - 1):
        F_y = cft['All'][i]
        E_1 += ((n - F_y) * F_y) / n
    # 计算E_2
    for j in range(cft.shape[1] - 1):
        for k in range(cft.shape[0] - 1):
            F_x = cft.iloc[cft.shape[0] - 1, j]
            f = cft.iloc[k, j]
            E_2 += ((F_x - f) * f) / F_x
    # 计算tauy
    tau_y = (E_1 - E_2) / E_1

    return tau_y


def ordinal_desc(df, col: str = ''):
    """
    功能：
    对传入的有序类型变量进行描述统计，生成表和柱状图。

    参数：
    df: DataFrame
    col: DataFrame中需要分析的变量，该变量为定类、定序变量

    2022-10-31 增加数量为0的类别的功能

    """
    # 初始化结果DataFrame
    result = pd.DataFrame()
    # 删除数量为0的类别
    df[col] = df[col].cat.remove_unused_categories()
    # 获得变量的内容
    result[f'{col}'] = df[col].value_counts(sort=False).index
    # 获得变量内容出现的次数
    result['个数'] = df[col].value_counts(sort=False).values
    # 获得各选项占比
    result['百分比'] = df[col].value_counts(normalize=True,
                                         sort=False).values * 100
    # 累计百分比
    result['累计百分比（%）'] = result['百分比'].values.cumsum()

    total_row = pd.Series({
        F"{col}": '总和',
        '个数': result['个数'].sum(),
        '百分比': result['百分比'].sum(),
        '累计百分比（%）': None,
    }).to_frame().T
    temp = pd.concat([result, total_row], ignore_index=True)
    temp['百分比'] = temp['百分比'].apply(lambda x: round(x, 2))
    temp['累计百分比（%）'] = temp['累计百分比（%）'].apply(lambda x: round(x, 2))
    # temp.fillna('', inplace=True)
    return temp


def set_plt():
    """ 图形参数设置 """
    # 设置字体
    plt.rcParams["font.sans-serif"] = ["SimHei"]
    # 该语句解决图像中的“-”负号的乱码问题
    plt.rcParams["axes.unicode_minus"] = False
    # 设置图形大小 大小的单位为英寸 inch
    plt.rcParams["figure.figsize"] = (6.4, 4.8)
    # 设置图形分辨率，出版物对图片的分辨率要求从150到300不等。
    plt.rcParams["figure.dpi"] = 200


def show_bar(df, col, sort=True):
    """
    功能：

    为数据框中给定的类别变量生成柱状图。

    参数：

    df： DataFrame

    col: DataFrame中的变量，应该为类别变量

    """

    set_plt()

    # 准备数据

    x = df[f'{col}'].value_counts(sort=sort).index
    y = df[f'{col}'].value_counts(sort=sort, normalize=True).values * 100

    # 绘图
    # 创建图
    fig, ax = plt.subplots()
    # 绘制柱状图
    rects1 = ax.bar(x, y)
    # 设定细节内容
    # 设置x轴变量名称
    ax.set_xlabel(f'{col}')
    # 设置y轴最大值
    ax.set_ylim(ymax=100)
    ax.bar_label(rects1, fmt="%.1f", padding=3)
    plt.show()


def gen_percent_table(df='', col='', sort=True):
    """ 生成类别变量的频数频率表 """
    b = pd.DataFrame()
    b[F'{col}'] = df[F'{col}'].value_counts(sort=sort).index
    b['个数'] = df[F'{col}'].value_counts(sort=sort).values
    b['百分比'] = df[F'{col}'].value_counts(sort=sort,
                                         normalize=True).values * 100
    total_row = pd.Series({
        F"{col}": '总和',
        '个数': b['个数'].sum(),
        '百分比': b['百分比'].sum(),
    }).to_frame().T
    # 联结两个dataframe
    temp = pd.concat([b, total_row], ignore_index=True)
    temp['百分比'] = temp['百分比'].apply(lambda x: round(x, 2))
    return temp


def read_spss(filename):
    """ 使用常用设置读取SPSS文件 """
    df0, metadata = pyreadstat.read_sav(filename, apply_value_formats=True)
    return df0, metadata


def print_all_cats(df):
    """ 打印Dataframe全部类别变量的取值及次数 """
    for col in df.columns[df.dtypes == 'category']:
        print(df[col].value_counts())


def print_all_int(df):
    """ 打印Dataframe全部int变量的取值范围 """
    for col in df.columns[df.dtypes == 'int']:
        print(df[col].describe())


def print_all_float(df):
    """ 打印Dataframe全部float变量的取值范围 """
    for col in df.columns[df.dtypes == 'float']:
        print(df[col].describe())
